/**
 * 读取文件内容生成文件摘要信息工具类
 */
package com.online.pay.common.sign;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.RandomAccessFile;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;

/**
 * FileSignUtil是SignSDK的入口类，其主要包括对文件的签名和验签两种操作，加签在文件尾添加签名信息，验签：验证带签名的文件签名是否正确。
 * <p>
 * SignSDK可以默认被ServerSDK读取，如果需要单独使用SignSDK，示例如下： <blockquote>
 * 
 * <pre>
 * 
 * FileSignUtil.sign(fileName);
 * FileSignUtil.verify(fileName);
 * </pre>
 * 
 * </blockquote> 注意不要并行地使用以上的签名方法，可能会带来内存耗尽等问题。
 * 
 * @author JiaYanFei wuhang
 *
 */
public class FileSignUtil {

	private static final String DEFAULT_ENCODE = "utf-8";
	// 签名记录标记
	private static final String SIGN_MARK = "00";
	// 数字签名标志
	private static final String SIGN_FLAG = "1";
	// 证书id 长度
	private static final int CERTID_LENGTH = 64;
	// 签名值长度
	private static final int SIGN_LENGTH = 256;

	/**
	 * 读取文件并生成字节数组
	 * 
	 * @author Jia YanFei 2016年9月12日
	 * @param fileName
	 *            文件名(绝对路径)
	 * @param signFlag
	 *            是否加签 true是加签读取全文件，false验签不包含最后一行的签名值
	 * @return
	 * @throws IOException
	 */
	private static byte[] readBytesFromFile(String fileName, boolean signFlag) throws IOException {
		BufferedReader br = null;
		FileInputStream fileInputStream = null;
		StringBuilder bf = new StringBuilder();
		String finalLine = getFinalLine(new File(fileName));
		byte[] b = new byte[0];
		try {
			fileInputStream = new FileInputStream(fileName);
			br = new BufferedReader(new InputStreamReader(fileInputStream));
			for (String line = br.readLine(); line != null; line = br.readLine()) {
				if (!signFlag && finalLine.equals(line)) {
					break;
				} else {
					bf.append(line);
					bf.append("\r\n");
				}
			}

			b = bf.toString().getBytes();
		} catch (IOException e) {
			throw e;
		} finally {
			if (br != null) {
				br.close();
			}

		}
		return b;
	}

	/**
	 * 根据文件生成摘要
	 * 
	 * @author Jia YanFei 2016年9月12日
	 * @param fileName
	 *            文件名
	 * @return
	 * @throws IOException
	 */
	private static String getFileHash(String fileName, boolean signFlag) throws IOException {
		byte buffer[];
		buffer = readBytesFromFile(fileName, signFlag);
		return sha1(buffer);
	}

	/**
	 * 根据字节数组生成摘要
	 * 
	 * @author Jia YanFei 2016年9月12日
	 * @param original
	 *            源字节数组
	 * @return
	 */
	private static String sha1(byte[] original) {
		MessageDigest msgDigest = null;
		try {
			msgDigest = MessageDigest.getInstance("SHA1");
			msgDigest.update(original);
		} catch (NoSuchAlgorithmException e) {
			throw new IllegalStateException("System doesn't support SHA1 algorithm.");
		}

		return bytesToHexString(msgDigest.digest());
	}

	/**
	 * 字节数组转换为16进制字符串
	 * 
	 * @author Jia YanFei 2016年9月12日
	 * @param bytes
	 * @return
	 */
	private static String bytesToHexString(byte[] bytes) {
		byte tb;
		char low;
		char high;
		char tmpChar;
		StringBuilder str = new StringBuilder();

		for (int i = 0; i < bytes.length; i++) {
			tb = bytes[i];
			tmpChar = (char) ((tb >>> 4) & 0x000f);
			if (tmpChar >= 10) {
				high = (char) (('a' + tmpChar) - 10);
			} else {
				high = (char) ('0' + tmpChar);
			}
			str.append(high);
			tmpChar = (char) (tb & 0x000f);
			if (tmpChar >= 10) {
				low = (char) (('a' + tmpChar) - 10);
			} else {
				low = (char) ('0' + tmpChar);
			}
			str.append(low);
		}
		return str.toString();
	}

	/**
	 * 获取每个文件最后一行不为空的数据
	 * 
	 * @author JYF
	 * @throws IOException
	 * 
	 */
	private static String getFinalLine(File file) throws IOException {
		if (file == null || !file.exists() || file.isDirectory()) {
			throw new RuntimeException("参数不是文件路径");
		}
		String line = null;
		RandomAccessFile rf = null;
		try {
			rf = new RandomAccessFile(file, "r");
			long len = rf.length();
			long start = rf.getFilePointer();
			long nextend = start + len - 1;

			rf.seek(nextend);
			int c = -1;
			int t = 0;
			while (nextend > start) {
				c = rf.read();
				if (c == '\n' || c == '\r') {
					line = rf.readLine();
					t++;
					if (t >= 1 && line != null && !line.equals("")) {

						return line;
					}
					nextend--;
				}
				nextend--;
				rf.seek(nextend);
				if (nextend == 0) {// 当文件指针退至文件开始处，输出第一行
					System.out.println(rf.readLine());
				}
			}

		} finally {

			if (rf != null) {
				rf.close();
			}
		}
		return line;
	}

	/**
	 * 文件签名
	 * 
	 * @param signFileName
	 *            待签名的文件名
	 * @return String - 文件最后一行是含签名的完整字符串（共323位：2位记录标记、1位签名标记、64位证书标识、256位签名值）
	 * @throws IOException
	 *             如果文件不存在或者其他IO异常
	 */
	public static String sign(String signFileName) throws IOException {

		// 获取文件摘要
		String plainText = getFileHash(signFileName, true);//
		// 获取证书id
		String certId = CertificateCoder.getSignCerId();
		;
		// 调用加签
		byte[] signBytes = CertificateCoder.sign(plainText.getBytes(DEFAULT_ENCODE));
		// Base64编码
		String signStr = Base64Util.encode(signBytes);
		// 按照签名格式组合成最终签名
		String signFinalStr = SIGN_MARK + SIGN_FLAG
				+ String.format("%" + CERTID_LENGTH + "s" + "%" + SIGN_LENGTH + "s", certId, signStr);
		// 签名串添加到位尾
		FileOutputStream fos = null;
		try {
			fos = new FileOutputStream(signFileName, true);
			fos.write(signFinalStr.getBytes("utf-8"));
		} finally {
			if (null != fos) {
				fos.flush();
				fos.close();
			}

		}

		return signFinalStr;
	}

	/**
	 * 含签名的文件验签 文件最后一行是含签名的完整字符串（共323位：2位记录标记、1位签名标记、64位证书标识、256位签名值）
	 * 
	 * @author Jia YanFei 2016年9月13日
	 * @param fileName
	 *            文件的绝对路径
	 * @return true表示验签成功， false表示验签失败
	 * @throws IOException
	 *             如果文件不存在或者其他IO异常
	 */
	public static boolean verifySign(String fileName) throws IOException {
		// 获取最后一行签名值
		String signature = getFinalLine(new File(fileName));
		// 获取文件摘要
		String plainText = getFileHash(fileName, false);
		boolean b = false;
		String signMark = signature.substring(0, 2);
		String signflag = signature.substring(2, 3);
		// 判断签名标记
		b = signMark.equals(SIGN_MARK) && (signflag.equals(SIGN_FLAG));
		// 调用验签服务
		b = b && CertificateCoder.verify(plainText.getBytes(DEFAULT_ENCODE),
				Base64Util.decode(signature.substring(67, 323).trim()));

		return b;

	}

}
